--
--	Synlight Lua Module created by Henry Tran
--

-- Enums
CHUNK_STRING = 1 -- Anything enclosed by quotes or double brackets.
CHUNK_NUMBER = 2 -- Self explanatory.
CHUNK_NEWLINE = 3 -- Any occurance of \n.
CHUNK_WHITESPACE = 4 -- Any whitespaces.
CHUNK_COMMENT = 5 -- Any comment.
CHUNK_WORD = 6 -- Any variables or global variables.
CHUNK_NONWORD = 7 -- Any undefined non-word variable. Usually would occur due to an error.
CHUNK_BRACKET = 8 -- Self explanatory.
CHUNK_SPECIALWORD = 9 -- local, function, if, then, else, end
CHUNK_GLOBALVARIABLES = 10 -- _G, _R, Msg, MsgN, print, pairs
CHUNK_OPERATOR = 11 -- +, -, ^, %, /, =, ==, >, <
CHUNK_HOOK = 12 -- GM:PlayerSpawn(), ENT:Initialize, etc.
CHUNK_LIBRARY = 13 -- surface.DrawText, draw.SimpleText, etc.
CHUNK_ENUM = 14 -- IN_JUMP, IN_SPEED, etc.
CHUNK_CLASSFUNCTION = 15 -- :Give, :SetModel, :SetHealth, etc.
CHUNK_SPLIT = 16 -- If a split is passed as an argument, a split will be inserted wherever it is defined.
CHUNK_TESTVARIABLES = 17 -- true, false, nil

local _G, pairs, tonumber, string, table, print, tostring, type = _G, pairs, tonumber, string, table, print, tostring, type

module "synlight"

--[[

If anything's missing, you can feel free to add it here.

]]

local libaries = {
	surface = {
		"DrawText",
		"DrawSetTexture"
	},
	draw = {
	},
	table = {
	},
	string = {
	},
	util = {
		"PrecacheModel"
	},
	player_manager = {
		"TranslatePlayerModel"
	}
}

local hooks = {
	GM = {
		"PlayerSpawn"
	},
	ENT = {
	},
	EFFECT = {
	}
}

local enums = {
"IN_JUMP",
"IN_SPEED"
}

local class_functions = {
"Give",
"GetInfo",
"SetModel",
"SetMaxHealth",
"SetHealth",
"SetWalkSpeed",
"SetRunSpeed",
"SprintDisable",
"KillSilent"
}

local special_words = {
"local",
"function",
"end",
"if",
"then",
"else",
"end",
"while",
"for",
"in",
"return",
"and",
"not",
"continue",
"break",
"do",
"or"
}

local test_variables = {
"true",
"false",
"nil"
}

local global_variables = {
"_G",
"_R",
"Msg",
"MsgN",
"print",
"pairs",
"module",
"require",
"include",
"AddCSLuaFile",
"tobool",
"tonumber",
"tostring"
}

local splitting_chars = {
";",
"%[",
"%]",
"%(",
"%)",
"{",
"}",
",",
" ",
"	",
"\n",
"%.",
"-",
"+",
"*",
":",
"\"",
"%s",
"=",
"/",
"\\",
"%%",
"%^"
}

local brackets = {
"[",
"{",
"(",
"]",
"}",
")"
}

local one_line_string = {
{ "\"", "\"" },
{ "'", "'" }
}

local strings = {
--{ "[[", "]]" }
}

local comments = {
{ "--", "\n" },
{ "//", "\n" },
{ "/*", "*/" }--[=[,
{ "--[[", "]]" }]=]
}

local operators = {
"+",
"-",
"^",
"%",
"=",
">",
"<",
"*",
"/",
".",
":",
";",
"!",
"~",
","
}

local whitespaces = {
"	"
}

local in_comment = false
local in_string = false
local in_string_break = false
local is_one_line_string = false
local in_multi_string = false

local count = 0
local splits = {}

local word_callbacks = {}

function registerCallback( name, funct )
	if type( funct ) != "function" then
		error( "Expecting 'function' for argument 2, recieved '"..type( funct ).."'", 2 )
	elseif type( name ) != "string" then
		error( "Expecting 'string' for argument 1, recieved '"..type( name ).."'", 2 )
	end
	word_callbacks[name] = funct
end

local function runcallbacks( chunk, last_word, character, count )
	local CallbackTable = {
		last_word = last_word,
		class_character = character,
		position = count + 1
	}
	for k,v in pairs ( word_callbacks ) do
		local new_chunk = v( chunk, CallbackTable )
		if new_chunk then
			add_chunk_to_final( chunk, new_chunk )
			return true
		end
	end
end

local final = {}
local merged_splitting_chars = ""
for _,v in pairs( splitting_chars ) do
	merged_splitting_chars = merged_splitting_chars .. v
end

local function process_chunk_to_final( chunk, chunk_type )

	if ( final[#final] and final[#final][2] == chunk_type ) then
		final[#final][1] = final[#final][1] .. chunk
	else
		final[#final+1] = {chunk, chunk_type}
	end
	
end

local function add_chunk_to_final( chunk, chunk_type )

	local not_split = true
	local nonsplits = {}
	local last_split = chunk
	local i = 1
	while ( splits[i] ) do
		split = splits[i]
		if count + string.len( chunk ) > split and count <= split then
			local new_chunk = string.sub( last_split, 0, split - count )
			table.insert( nonsplits, new_chunk )
			last_split = string.sub( last_split, split - count + 1, -1 )
			count = count + string.len( new_chunk )
			table.remove( splits, i )
			not_split = false
			i = i - 1
		end
		i = i + 1
	end
	
	if not_split then
		process_chunk_to_final( chunk, chunk_type )
		count = count + string.len( chunk )
	else
		for _,nonsplit in pairs( nonsplits ) do
			process_chunk_to_final( nonsplit, chunk_type )
			process_chunk_to_final( "", _G.CHUNK_SPLIT )
		end
		count = count + string.len( last_split )
		process_chunk_to_final( last_split, chunk_type )
	end
			
end

local function modify_previous_chunk_in_final( chunk_size, chunk_type )

	if chunk_size == 0 then return end
	
	local to_delete_size = chunk_size
	local chunk_id = #final
	local str = ""
	while ( to_delete_size >= 0 ) do
		local cur_chunk = final[chunk_id][1]
		if not final[chunk_id] then break end
		if ( string.len( cur_chunk ) < to_delete_size ) then
			str = cur_chunk .. str
			table.remove( final, chunk_id )
			to_delete_size = to_delete_size - string.len( cur_chunk )
		else
			str = string.sub( cur_chunk, -to_delete_size, -1 ) .. str
			final[chunk_id][1] = string.sub( cur_chunk, 1, -to_delete_size-1 )
			to_delete_size = 0
			if ( string.len( final[chunk_id][1] ) == 0 ) then
				table.remove( final, chunk_id )
			end
			break
		end
		chunk_id = chunk_id - 1
	end
	
	add_chunk_to_final( str, chunk_type )

end

local string_iscomment = false
local string_len = 0
local string_stage = 0
local function check_multistring( tiny )
	if tiny == "[" and string_stage == 0 then
		string_len = 0
		string_stage = 1
	elseif tiny == "=" and string_stage == 1 then
		string_len = string_len + 1
	elseif tiny == "[" and string_stage == 1 then
		string_stage = 1
		return true, string_len
	else
		if not in_comment then
			string_iscomment = false
		end
		string_len = 0
		if tiny == "[" then
			string_stage = 1
		else
			string_stage = 0
		end
	end
	return false
end

local string_end_len = 0
local string_end_stage = 0
local function check_multistring_end( tiny )
	if tiny == "]" and string_end_stage == 0 then
		string_end_len = 0
		string_end_stage = 1
	elseif tiny == "=" and string_end_stage == 1 then
		string_end_len = string_end_len + 1
	elseif tiny == "]" and string_end_stage == 1 then
		string_end_stage = 1
		string_stage = 0
		string_len = 0
		return true, string_end_len
	else
		string_end_len = 0
		if tiny == "]" then
			string_end_stage = 1
		else
			string_end_stage = 0
		end
	end
	return false
end

local function previous( before, chars )

	local str = ""
	for i=0,string.len( chars )-1 do
		if not before[#before-i] then return false end
		str = before[#before-i] .. str
	end

	return str == chars

end

local function identify_nonwords( nonword )
	local before = {}
	local matched_string = {}
	local str_start = 0
	local com_start = 0
	for tiny in string.gmatch( nonword,"." ) do

		local cont = true -- Oh Lua, how you don't have a continue function.
		
		table.insert( before, tiny )
		
		com_start = com_start + 1
		str_start = str_start + 1
		
		if not( tiny == " " or tiny == ":" or tiny == "." ) then
			last_library = false
			last_word_is_library = false
			last_hook = false
			last_word_is_hook = false
			last_classfunction = false
			last_callback = false
		end
		
		local multistring, multistring_len = check_multistring( tiny )
		local does_prev_signify_beginning_of_comment = previous( before, "--" )
		if does_prev_signify_beginning_of_comment and not in_comment and not in_string then
			string_iscomment = true
			string_justchanged = true
		end
		
		if multistring and cont and not in_string and ( not in_comment or string_iscomment ) and not in_multi_string then
			if string_iscomment then
				modify_previous_chunk_in_final( multistring_len+1, _G.CHUNK_COMMENT )
				add_chunk_to_final( tiny, _G.CHUNK_COMMENT )
				in_comment = -1
			else
				modify_previous_chunk_in_final( multistring_len+1, _G.CHUNK_STRING )
				add_chunk_to_final( tiny, _G.CHUNK_STRING )
				in_string = -1
			end
			in_multi_string = multistring_len
			cont = false
		end
		
		local multistring, multistring_len = check_multistring_end( tiny )
		if in_multi_string and cont and ( in_string or in_comment ) then
			if multistring and in_multi_string == multistring_len then
				if string_iscomment then
					add_chunk_to_final( tiny, _G.CHUNK_COMMENT )
					in_comment = false
				elseif in_string and not string_iscomment then
					add_chunk_to_final( tiny, _G.CHUNK_STRING )
					in_string = false
				else
					add_chunk_to_final( tostring(in_comment).." "..tostring(in_string).." "..tostring(string_iscomment), _G.CHUNK_COMMENT )
					in_comment = false
					in_string = false
				end
				in_multi_string = false
				string_len = 0
				string_end_len = 0
				string_iscomment = false
				cont = false
			end
		end
		
		if not string_justchanged and not ( string_stage > 0 ) and not in_multi_string then
			string_iscomment = false
		end
		
		string_justchanged = false
		
		if cont then
			if cont and in_comment and comments[in_comment] then
				if previous( before, comments[in_comment][2] ) then
					modify_previous_chunk_in_final( string.len( comments[in_comment][2] )-1, _G.CHUNK_COMMENT )
					if tiny == "\n" then
						add_chunk_to_final( tiny, _G.CHUNK_NEWLINE )
					else
						add_chunk_to_final( tiny, _G.CHUNK_COMMENT )
					end
					in_comment = false
					cont = false
				end
			end
			
			if cont and not in_string and not in_comment then
				for comment_type,nonword_match in pairs( comments ) do
					if previous( before, nonword_match[1] ) then
						modify_previous_chunk_in_final( string.len( nonword_match[1] )-1, _G.CHUNK_COMMENT )
						add_chunk_to_final( tiny, _G.CHUNK_COMMENT )
						in_comment = comment_type
						com_start = string.len( nonword_match[1] )
						cont = false
					end
				end
			end
			
			for _,nonword_match in pairs( whitespaces ) do
				if previous( before, nonword_match ) then
					modify_previous_chunk_in_final( string.len( nonword_match )-1, _G.CHUNK_WHITESPACE )
					add_chunk_to_final( tiny, _G.CHUNK_WHITESPACE )
					cont = false
				end
			end
			
			if cont and in_string and one_line_string[in_string] then
				if ( previous( before, one_line_string[in_string][2] ) or tiny == "\n" )/* and is_one_line_string*/ then
					modify_previous_chunk_in_final( string.len( one_line_string[in_string][2] )-1, _G.CHUNK_STRING )
					if tiny == "\n" then
						add_chunk_to_final( tiny, _G.CHUNK_NEWLINE )
					else
						add_chunk_to_final( tiny, _G.CHUNK_STRING )
					end
					in_string = false
					cont = false
				end
			end
			
			if cont and not in_string and not in_comment then
				for string_type,nonword_match in pairs( one_line_string ) do
					if previous( before, nonword_match[1] ) then
						modify_previous_chunk_in_final( string.len( nonword_match[1] )-1, _G.CHUNK_STRING )
						add_chunk_to_final( tiny, _G.CHUNK_STRING )
						in_string = string_type
						cont = false
					end
				end
			end
			
			if cont then
			
				if tiny == "\n" then
					add_chunk_to_final( tiny, _G.CHUNK_NEWLINE )
					cont = false
				end
				
				if cont then
					if in_comment then
						add_chunk_to_final( tiny, _G.CHUNK_COMMENT )
						cont = false
					end
					
					if in_string and cont then
						add_chunk_to_final( tiny, _G.CHUNK_STRING )
						cont = false
					end
				
					if cont and tiny == ":" or "." then
						if runcallbacks( tiny, last_word, tiny, count ) then
							cont = false
						end
					end
					
					if cont then
						for _,nonword_match in pairs( operators ) do
							if previous( before, nonword_match ) then
								modify_previous_chunk_in_final( string.len( nonword_match )-1, _G.CHUNK_OPERATOR )
								if tiny == "." and last_word_is_library then
									add_chunk_to_final( tiny, _G.CHUNK_LIBRARY )
									last_library = tiny
								elseif ( tiny == ":" or tiny == "." ) and last_word_is_hook then
									add_chunk_to_final( tiny, _G.CHUNK_HOOK )
									last_hook = tiny
								elseif tiny == ":" then
									add_chunk_to_final( tiny, _G.CHUNK_OPERATOR )
									last_classfunction = ":"
								elseif tiny == ":" or tiny == "." then
									add_chunk_to_final( tiny, _G.CHUNK_OPERATOR )
									last_callback = tiny
								else
									add_chunk_to_final( tiny, _G.CHUNK_OPERATOR )
								end
								cont = false
							end
						end
					
						if cont then
							for _,nonword_match in pairs( brackets ) do
								if previous( before, nonword_match ) then
									modify_previous_chunk_in_final( string.len( nonword_match )-1, _G.CHUNK_BRACKET )
									add_chunk_to_final( tiny, _G.CHUNK_BRACKET )
									cont = false
								end
							end
							
							if cont then
								add_chunk_to_final( tiny, _G.CHUNK_NONWORD )
							end
						end
					end
				end
			end
		end
	end

end

local function identify_word( word )

	if runcallbacks( word, last_word, last_library or last_hook or last_callback, count ) then
		return
	end

	if libaries[word] then
		add_chunk_to_final( word, _G.CHUNK_LIBRARY )
		last_word_is_library = word
		return
	end
	
	if last_word_is_library and last_library == "." and libaries[last_word_is_library] then
		for _,word_match in pairs ( libaries[last_word_is_library] ) do
			if word_match == word then
				add_chunk_to_final( word, _G.CHUNK_LIBRARY )
				last_word_is_library = false
				return
			end
		end
	end
	
	last_word_is_library = false

	if hooks[word] then
		add_chunk_to_final( word, _G.CHUNK_HOOK )
		last_word_is_hook = word
		return
	end
	
	if last_word_is_hook and last_hook and hooks[last_word_is_hook] then
		for _,word_match in pairs ( hooks[last_word_is_hook] ) do
			if word_match == word then
				add_chunk_to_final( word, _G.CHUNK_HOOK )
				last_word_is_hook = false
				return
			end
		end
	end
	
	last_word_is_hook = false

	if last_classfunction then
		for _,word_match in pairs( class_functions ) do
			if word == word_match then
				add_chunk_to_final( word, _G.CHUNK_CLASSFUNCTION )
				last_classfunction = false
				return
			end
		end
	end
	
	last_classfunction = false
	
	if tonumber( word ) then
		add_chunk_to_final( word, _G.CHUNK_NUMBER )
		return
	end

	for _,word_match in pairs( special_words ) do
		if word == word_match then
			add_chunk_to_final( word, _G.CHUNK_SPECIALWORD )
			return
		end
	end
	
	for _,word_match in pairs( global_variables ) do
		if word == word_match then
			add_chunk_to_final( word, _G.CHUNK_GLOBALVARIABLES )
			return
		end
	end
	
	for _,word_match in pairs ( test_variables ) do
		if word == word_match then
			add_chunk_to_final( word, _G.CHUNK_TESTVARIABLES )
			return
		end
	end
	
	for _,word_match in pairs( enums ) do
		if word == word_match then
			add_chunk_to_final( word, _G.CHUNK_ENUM )
			return
		end
	end
	
	add_chunk_to_final( word, _G.CHUNK_WORD )
end

function parse( str, split )
	if type( str ) != "string" then
		error( "Expecting 'string' for argument 1, recieved '"..type( str ).."'", 2 )
	elseif split and type( split ) != "table" then
		error( "Expecting 'table' for argument 2, recieved '"..type( split ).."'", 2 )
	end
	splits = split or {}
	local start, endpos, word = nil, 0, true
	local endpos_before
	in_comment = false
	in_string = false
	in_string_break = false
	is_one_line_string = false
	in_multi_string = false
	string_len = 0
	string_stage = 0
	string_end_len = 0
	string_end_stage = 0
	string_iscomment = false
	string_justchanged = false
	
	last_word_is_library = false
	last_library = false

	last_word_is_hook = false
	last_hook = false
	
	last_word = ""
	last_callback = false

	last_classfunction = false
	
	final = {}
	
	count = 0
	while ( word ) do
		endpos_before = endpos
		start, endpos, word = string.find( str, "([^"..merged_splitting_chars.."]+)", endpos+1 )
		if start then
			identify_nonwords( string.sub( str, endpos_before+1, start-1 ) )
		end
		if ( word ) then
			if in_comment then
				add_chunk_to_final( word, _G.CHUNK_COMMENT )
			elseif in_string then
				add_chunk_to_final( word, _G.CHUNK_STRING )
			else
				identify_word( word )
				last_word = word
			end
		end
	end
	if endpos_before then
		identify_nonwords( string.sub( str, endpos_before+1, -1 ) )
	end
	
	return final
end

trans = { "CHUNK_STRING", "CHUNK_NUMBER", "CHUNK_NEWLINE", "CHUNK_WHITESPACE", "CHUNK_COMMENT", "CHUNK_WORD", "CHUNK_NONWORD", "CHUNK_BRACKET", "CHUNK_SPECIALWORD", "CHUNK_GLOBALVARIABLES", "CHUNK_OPERATOR" }
